// json5.js
// Modern JSON. See README.md for details.
//
// This file is based directly off of Douglas Crockford's json_parse.js:
// https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
const s = 'zzz'
var JSON5 = typeof exports === 'object' ? exports : {}

JSON5.parse = (function () {
  'use strict'

  // This is a function that can parse a JSON5 text, producing a JavaScript
  // data structure. It is a simple, recursive descent parser. It does not use
  // eval or regular expressions, so it can be used as a model for implementing
  // a JSON5 parser in other languages.

  // We are defining the function inside of another function to avoid creating
  // global variables.

  var at, // The index of the current character
    lineNumber, // The current line number
    columnNumber, // The current column number
    ch, // The current character
    escapee = {
      "'": "'",
      '"': '"',
      '\\': '\\',
      '/': '/',
      '\n': '', // Replace escaped newlines in strings w/ empty string
      b: '\b',
      f: '\f',
      n: '\n',
      r: '\r',
      t: '\t'
    },
    ws = [' ', '\t', '\r', '\n', '\v', '\f', '\xA0', '\uFEFF'],
    text,
    renderChar = function (chr) {
      return chr === '' ? 'EOF' : "'" + chr + "'"
    },
    error = function (m) {
      // Call error when something is wrong.

      var error = new SyntaxError()
      // beginning of message suffix to agree with that provided by Gecko - see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
      error.message =
        m +
        ' at line ' +
        lineNumber +
        ' column ' +
        columnNumber +
        ' of the JSON5 data. Still to read: ' +
        JSON.stringify(text.substring(at - 1, at + 19))
      error.at = at
      // These two property names have been chosen to agree with the ones in Gecko, the only popular
      // environment which seems to supply this info on JSON.parse
      error.lineNumber = lineNumber
      error.columnNumber = columnNumber
      throw error
    },
    next = function (c) {
      // If a c parameter is provided, verify that it matches the current character.

      if (c && c !== ch) {
        error('Expected ' + renderChar(c) + ' instead of ' + renderChar(ch))
      }

      // Get the next character. When there are no more characters,
      // return the empty string.

      ch = text.charAt(at)
      at++
      columnNumber++
      if (ch === '\n' || (ch === '\r' && peek() !== '\n')) {
        lineNumber++
        columnNumber = 0
      }
      return ch
    },
    peek = function () {
      // Get the next character without consuming it or
      // assigning it to the ch varaible.

      return text.charAt(at)
    },
    identifier = function () {
      // Parse an identifier. Normally, reserved words are disallowed here, but we
      // only use this for unquoted object keys, where reserved words are allowed,
      // so we don't check for those here. References:
      // - http://es5.github.com/#x7.6
      // - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables
      // - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
      // TODO Identifiers can have Unicode "letters" in them; add support for those.

      var key = ch

      // Identifiers must start with a letter, _ or $.
      if (
        ch !== '_' &&
        ch !== '$' &&
        (ch < 'a' || ch > 'z') &&
        (ch < 'A' || ch > 'Z')
      ) {
        error('Bad identifier as unquoted key')
      }

      // Subsequent characters can contain digits.
      while (
        next() &&
        (ch === '_' ||
          ch === '$' ||
          (ch >= 'a' && ch <= 'z') ||
          (ch >= 'A' && ch <= 'Z') ||
          (ch >= '0' && ch <= '9'))
      ) {
        key += ch
      }

      return key
    },
    number = function () {
      // Parse a number value.

      var number,
        sign = '',
        string = '',
        base = 10

      if (ch === '-' || ch === '+') {
        sign = ch
        next(ch)
      }

      // support for Infinity (could tweak to allow other words):
      if (ch === 'I') {
        number = word()
        if (typeof number !== 'number' || isNaN(number)) {
          error('Unexpected word for number')
        }
        return sign === '-' ? -number : number
      }

      // support for NaN
      if (ch === 'N') {
        number = word()
        if (!isNaN(number)) {
          error('expected word to be NaN')
        }
        // ignore sign as -NaN also is NaN
        return number
      }

      if (ch === '0') {
        string += ch
        next()
        if (ch === 'x' || ch === 'X') {
          string += ch
          next()
          base = 16
        } else if (ch >= '0' && ch <= '9') {
          error('Octal literal')
        }
      }

      switch (base) {
        case 10:
          while (ch >= '0' && ch <= '9') {
            string += ch
            next()
          }
          if (ch === '.') {
            string += '.'
            while (next() && ch >= '0' && ch <= '9') {
              string += ch
            }
          }
          if (ch === 'e' || ch === 'E') {
            string += ch
            next()
            if (ch === '-' || ch === '+') {
              string += ch
              next()
            }
            while (ch >= '0' && ch <= '9') {
              string += ch
              next()
            }
          }
          break
        case 16:
          while (
            (ch >= '0' && ch <= '9') ||
            (ch >= 'A' && ch <= 'F') ||
            (ch >= 'a' && ch <= 'f')
          ) {
            string += ch
            next()
          }
          break
      }

      if (sign === '-') {
        number = -string
      } else {
        number = +string
      }

      if (!isFinite(number)) {
        error('Bad number')
      } else {
        return number
      }
    },
    string = function () {
      // Parse a string value.

      var hex,
        i,
        string = '',
        delim, // double quote or single quote
        uffff

      // When parsing for string values, we must look for ' or " and \ characters.

      if (ch === '"' || ch === "'") {
        delim = ch
        while (next()) {
          if (ch === delim) {
            next()
            return string
          } else if (ch === '\\') {
            next()
            if (ch === 'u') {
              uffff = 0
              for (i = 0; i < 4; i += 1) {
                hex = parseInt(next(), 16)
                if (!isFinite(hex)) {
                  break
                }
                uffff = uffff * 16 + hex
              }
              string += String.fromCharCode(uffff)
            } else if (ch === '\r') {
              if (peek() === '\n') {
                next()
              }
            } else if (typeof escapee[ch] === 'string') {
              string += escapee[ch]
            } else {
              break
            }
          } else if (ch === '\n') {
            // unescaped newlines are invalid; see:
            // https://github.com/aseemk/json5/issues/24
            // TODO this feels special-cased; are there other
            // invalid unescaped chars?
            break
          } else {
            string += ch
          }
        }
      }
      error('Bad string')
    },
    inlineComment = function () {
      // Skip an inline comment, assuming this is one. The current character should
      // be the second / character in the // pair that begins this inline comment.
      // To finish the inline comment, we look for a newline or the end of the text.

      if (ch !== '/') {
        error('Not an inline comment')
      }

      do {
        next()
        if (ch === '\n' || ch === '\r') {
          next()
          return
        }
      } while (ch)
    },
    blockComment = function () {
      // Skip a block comment, assuming this is one. The current character should be
      // the * character in the /* pair that begins this block comment.
      // To finish the block comment, we look for an ending */ pair of characters,
      // but we also watch for the end of text before the comment is terminated.

      if (ch !== '*') {
        error('Not a block comment')
      }

      do {
        next()
        while (ch === '*') {
          next('*')
          if (ch === '/') {
            next('/')
            return
          }
        }
      } while (ch)

      error('Unterminated block comment')
    },
    comment = function () {
      // Skip a comment, whether inline or block-level, assuming this is one.
      // Comments always begin with a / character.

      if (ch !== '/') {
        error('Not a comment')
      }

      next('/')

      if (ch === '/') {
        inlineComment()
      } else if (ch === '*') {
        blockComment()
      } else {
        error('Unrecognized comment')
      }
    },
    white = function () {
      // Skip whitespace and comments.
      // Note that we're detecting comments by only a single / character.
      // This works since regular expressions are not valid JSON(5), but this will
      // break if there are other valid values that begin with a / character!

      while (ch) {
        if (ch === '/') {
          comment()
        } else if (ws.indexOf(ch) >= 0) {
          next()
        } else {
          return
        }
      }
    },
    word = function () {
      // true, false, or null.

      switch (ch) {
        case 't':
          next('t')
          next('r')
          next('u')
          next('e')
          return true
        case 'f':
          next('f')
          next('a')
          next('l')
          next('s')
          next('e')
          return false
        case 'n':
          next('n')
          next('u')
          next('l')
          next('l')
          return null
        case 'I':
          next('I')
          next('n')
          next('f')
          next('i')
          next('n')
          next('i')
          next('t')
          next('y')
          return Infinity
        case 'N':
          next('N')
          next('a')
          next('N')
          return NaN
      }
      error('Unexpected ' + renderChar(ch))
    },
    value, // Place holder for the value function.
    array = function () {
      // Parse an array value.

      var array = []

      if (ch === '[') {
        next('[')
        white()
        while (ch) {
          if (ch === ']') {
            next(']')
            return array // Potentially empty array
          }
          // ES5 allows omitting elements in arrays, e.g. [,] and
          // [,null]. We don't allow this in JSON5.
          if (ch === ',') {
            error('Missing array element')
          } else {
            array.push(value())
          }
          white()
          // If there's no comma after this value, this needs to
          // be the end of the array.
          if (ch !== ',') {
            next(']')
            return array
          }
          next(',')
          white()
        }
      }
      error('Bad array')
    },
    object = function () {
      // Parse an object value.

      var key,
        object = {}

      if (ch === '{') {
        next('{')
        white()
        while (ch) {
          if (ch === '}') {
            next('}')
            return object // Potentially empty object
          }

          // Keys can be unquoted. If they are, they need to be
          // valid JS identifiers.
          if (ch === '"' || ch === "'") {
            key = string()
          } else {
            key = identifier()
          }

          white()
          next(':')
          object[key] = value()
          white()
          // If there's no comma after this pair, this needs to be
          // the end of the object.
          if (ch !== ',') {
            next('}')
            return object
          }
          next(',')
          white()
        }
      }
      error('Bad object')
    }

  value = function () {
    // Parse a JSON value. It could be an object, an array, a string, a number,
    // or a word.

    white()
    switch (ch) {
      case '{':
        return object()
      case '[':
        return array()
      case '"':
      case "'":
        return string()
      case '-':
      case '+':
      case '.':
        return number()
      default:
        return ch >= '0' && ch <= '9' ? number() : word()
    }
  }

  // Return the json_parse function. It will have access to all of the above
  // functions and variables.

  return function (source, reviver) {
    var result

    text = String(source)
    at = 0
    lineNumber = 1
    columnNumber = 1
    ch = ' '
    result = value()
    white()
    if (ch) {
      error('Syntax error')
    }

    // If there is a reviver function, we recursively walk the new structure,
    // passing each name/value pair to the reviver function for possible
    // transformation, starting with a temporary root object that holds the result
    // in an empty key. If there is not a reviver function, we simply return the
    // result.

    return typeof reviver === 'function'
      ? (function walk(holder, key) {
          var k,
            v,
            value = holder[key]
          if (value && typeof value === 'object') {
            for (k in value) {
              if (Object.prototype.hasOwnProperty.call(value, k)) {
                v = walk(value, k)
                if (v !== undefined) {
                  value[k] = v
                } else {
                  delete value[k]
                }
              }
            }
          }
          return reviver.call(holder, key, value)
        })({ '': result }, '')
      : result
  }
})()

// JSON5 stringify will not quote keys where appropriate
JSON5.stringify = function (obj, replacer, space) {
  if (replacer && typeof replacer !== 'function' && !isArray(replacer)) {
    throw new Error('Replacer must be a function or an array')
  }
  var getReplacedValueOrUndefined = function (holder, key, isTopLevel) {
    var value = holder[key]

    // Replace the value with its toJSON value first, if possible
    if (value && value.toJSON && typeof value.toJSON === 'function') {
      value = value.toJSON()
    }

    // If the user-supplied replacer if a function, call it. If it's an array, check objects' string keys for
    // presence in the array (removing the key/value pair from the resulting JSON if the key is missing).
    if (typeof replacer === 'function') {
      return replacer.call(holder, key, value)
    } else if (replacer) {
      if (isTopLevel || isArray(holder) || replacer.indexOf(key) >= 0) {
        return value
      } else {
        return undefined
      }
    } else {
      return value
    }
  }

  function isWordChar(c) {
    return (
      (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c === '_' ||
      c === '$'
    )
  }

  function isWordStart(c) {
    return (
      (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || c === '$'
    )
  }

  function isWord(key) {
    if (typeof key !== 'string') {
      return false
    }
    if (!isWordStart(key[0])) {
      return false
    }
    var i = 1,
      length = key.length
    while (i < length) {
      if (!isWordChar(key[i])) {
        return false
      }
      i++
    }
    return true
  }

  // export for use in tests
  JSON5.isWord = isWord

  // polyfills
  function isArray(obj) {
    if (Array.isArray) {
      return Array.isArray(obj)
    } else {
      return Object.prototype.toString.call(obj) === '[object Array]'
    }
  }

  function isDate(obj) {
    return Object.prototype.toString.call(obj) === '[object Date]'
  }

  var objStack = []
  function checkForCircular(obj) {
    for (var i = 0; i < objStack.length; i++) {
      if (objStack[i] === obj) {
        throw new TypeError('Converting circular structure to JSON')
      }
    }
  }

  function makeIndent(str, num, noNewLine) {
    if (!str) {
      return ''
    }
    // indentation no more than 10 chars
    if (str.length > 10) {
      str = str.substring(0, 10)
    }

    var indent = noNewLine ? '' : '\n'
    for (var i = 0; i < num; i++) {
      indent += str
    }

    return indent
  }

  var indentStr
  if (space) {
    if (typeof space === 'string') {
      indentStr = space
    } else if (typeof space === 'number' && space >= 0) {
      indentStr = makeIndent(' ', space, true)
    } else {
      // ignore space parameter
    }
  }

  // Copied from Crokford's implementation of JSON
  // See https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195
  // Begin
  var cx =
      /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
    escapable =
      /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
    meta = {
      // table of character substitutions
      '\b': '\\b',
      '\t': '\\t',
      '\n': '\\n',
      '\f': '\\f',
      '\r': '\\r',
      '"': '\\"',
      '\\': '\\\\'
    }
  function escapeString(string) {
    // If the string contains no control characters, no quote characters, and no
    // backslash characters, then we can safely slap some quotes around it.
    // Otherwise we must also replace the offending characters with safe escape
    // sequences.
    escapable.lastIndex = 0
    return escapable.test(string)
      ? '"' +
          string.replace(escapable, function (a) {
            var c = meta[a]
            return typeof c === 'string'
              ? c
              : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
          }) +
          '"'
      : '"' + string + '"'
  }
  // End

  function internalStringify(holder, key, isTopLevel) {
    var buffer, res

    // Replace the value, if necessary
    var obj_part = getReplacedValueOrUndefined(holder, key, isTopLevel)

    if (obj_part && !isDate(obj_part)) {
      // unbox objects
      // don't unbox dates, since will turn it into number
      obj_part = obj_part.valueOf()
    }
    switch (typeof obj_part) {
      case 'boolean':
        return obj_part.toString()

      case 'number':
        if (isNaN(obj_part) || !isFinite(obj_part)) {
          return 'null'
        }
        return obj_part.toString()

      case 'string':
        return escapeString(obj_part.toString())

      case 'object':
        if (obj_part === null) {
          return 'null'
        } else if (isArray(obj_part)) {
          checkForCircular(obj_part)
          buffer = '['
          objStack.push(obj_part)

          for (var i = 0; i < obj_part.length; i++) {
            res = internalStringify(obj_part, i, false)
            buffer += makeIndent(indentStr, objStack.length)
            if (res === null || typeof res === 'undefined') {
              buffer += 'null'
            } else {
              buffer += res
            }
            if (i < obj_part.length - 1) {
              buffer += ','
            } else if (indentStr) {
              buffer += '\n'
            }
          }
          objStack.pop()
          if (obj_part.length) {
            buffer += makeIndent(indentStr, objStack.length, true)
          }
          buffer += ']'
        } else {
          checkForCircular(obj_part)
          buffer = '{'
          var nonEmpty = false
          objStack.push(obj_part)
          for (var prop in obj_part) {
            if (obj_part.hasOwnProperty(prop)) {
              var value = internalStringify(obj_part, prop, false)
              isTopLevel = false
              if (typeof value !== 'undefined' && value !== null) {
                buffer += makeIndent(indentStr, objStack.length)
                nonEmpty = true
                key = isWord(prop) ? prop : escapeString(prop)
                buffer += key + ':' + (indentStr ? ' ' : '') + value + ','
              }
            }
          }
          objStack.pop()
          if (nonEmpty) {
            buffer =
              buffer.substring(0, buffer.length - 1) +
              makeIndent(indentStr, objStack.length) +
              '}'
          } else {
            buffer = '{}'
          }
        }
        return buffer
      default:
        // functions and undefined should be ignored
        return undefined
    }
  }

  // special case...when undefined is used inside of
  // a compound object/array, return null.
  // but when top-level, return undefined
  var topLevelHolder = { '': obj }
  if (obj === undefined) {
    return getReplacedValueOrUndefined(topLevelHolder, '', true)
  }
  return internalStringify(topLevelHolder, '', true)
}
